package contegor.scire.structure;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import contegor.scire.core.CommitTask;
import contegor.scire.core.Concept;
import contegor.scire.core.ConceptImpl;
import contegor.scire.core.Connectable;
import contegor.scire.core.ConnectableImpl;
import contegor.scire.core.Container;
import contegor.scire.core.ContainerImpl;
import contegor.scire.core.DuplicateElementException;
import contegor.scire.core.Id;
import contegor.scire.core.IdFragment;
import contegor.scire.core.Identifiable;
import contegor.scire.core.InfiniteSingletonList;
import contegor.scire.core.Link;
import contegor.scire.core.Link.Flow;
import contegor.scire.core.LinkImpl;
import contegor.scire.core.MissingElementException;
import contegor.scire.core.NonDeletableElementException;
import contegor.scire.core.Primitive;
import contegor.scire.core.ReturnPoint;
import contegor.scire.core.ReturnPointAdapter;
import contegor.scire.core.ScireException;
import contegor.scire.core.Task;
import contegor.scire.core.TaskId;
import contegor.scire.core.TrackExternalLinks;
import contegor.scire.core.Transaction;
import contegor.scire.core.TransactionException;
import contegor.scire.core.TransactionId;
import contegor.scire.core.WithConsequences;
import contegor.scire.core.WithDependencies;
import contegor.scire.core.WrongStateException;
import contegor.scire.core.modifications.ChainCreateLink;
import contegor.scire.core.modifications.CreateConcept;
import contegor.scire.core.modifications.CreateLink;
import contegor.scire.core.modifications.CreatePrimitive;
import contegor.scire.core.modifications.DeleteConnectable;
import contegor.scire.core.modifications.Modification;
import contegor.scire.core.queries.GetConcepts;
import contegor.scire.core.queries.Query;

/**
 * A {@link Context} is the way to interact with a set of {@link ConceptCloud}s. Through it, it's possible to
 * execute queries against the {@link ConceptCloud}s at the other side of the {@link Connection}. Its results
 * will be stored inside this {@link Context}. This stored results can be modified and {@link Context} is
 * capable of forwarding this changes to the {@link ConceptCloud}s.
 * 
 * It's thread safe.
 *
 * @author  Héctor Cirbián Pousa
 */
public class Context {
  private static final Logger logger = LoggerFactory.getLogger(Context.class);
  private static final ReturnPoint<Object> logErrorReturnPoint = new ReturnPointAdapter<Object>() {
    public void manage(contegor.scire.core.TaskId taskId, Exception exception) {
      logger.error(exception.getMessage(), exception);
    };
  };

  protected static ThreadLocal<Boolean> transientTransaction = new ThreadLocal<Boolean>();

  /** Tags internal tasks so are never forwarded to the cloud.*/
  protected static interface InternalTask<R> extends Task<R> {};

  protected static class IncorporateConcept extends Modification<Concept> implements InternalTask<Concept>, WithDependencies<Concept, Concept>, WithConsequences<Concept, Void> {
    private static final long serialVersionUID = -1520033456372936349L;

    protected Concept concept;
    protected List<IncorporateConcept> dependencies;
    protected List<TrackExternalLinks> consequences;

    public IncorporateConcept(TaskId taskId, Concept concept) {
      super(concept.getId().getParentId());
      this.taskId = taskId;
      this.concept = concept;
      this.dependencies = null;
      this.consequences = null;
      generateIncorporateDummies(concept);
    }

    public IncorporateConcept(TaskId taskId, Id targetContainerId, Concept concept) {
      super(targetContainerId);
      this.taskId = taskId;
      this.concept = concept;
      this.dependencies = null;
      this.consequences = null;
      generateIncorporateDummies(concept);
    }

    protected void generateIncorporateDummies(Connectable connectable) {
      for (Link link: connectable.getLinks().values()) {
        generateIncorporateDummies(link);
        Id target = ((Link)link).getTarget();
        if (!Id.RAW_ROOT.isAncestorOf(target)) {
          if (this.dependencies == null)
            this.dependencies = new LinkedList<IncorporateConcept>();
          this.dependencies.add(new IncorporateConcept(this.taskId.next(true), new Dummy(target)));
        }
      }
    }

    protected void trackLinks(ContextContainerImpl container, Connectable connectable) {
      for (Link link: connectable.getLinks().values()) {
        trackLinks(container, link);
        Id target = ((Link)link).getTarget();
        if (!Id.RAW_ROOT.isAncestorOf(target)) {
          container.addReverseLinkReference((LinkImpl)link, target);
          if (!(container.getId().isParentOf(target) || Id.RAW_ROOT.isAncestorOf(target))) {
            if (this.consequences == null)
              this.consequences = new LinkedList<TrackExternalLinks>();
            TrackExternalLinks trackExternalLinks = new TrackExternalLinks(target.getParentId(), target.getLastIdFragment(), container.getId());
            trackExternalLinks.setTaskId(this.taskId.next(true));
            this.consequences.add(trackExternalLinks);
          }
        }
      }
    }

    @Override
    public void process(Container targetContainer) throws ScireException {
      ContextContainerImpl contextContainer = (ContextContainerImpl)targetContainer;
      if (contextContainer.incorporate(concept))
        trackLinks(contextContainer, concept);
    }

    @Override
    public List<? extends Task<Concept>> getDependencies() {
      return this.dependencies;
    }

    @Override
    public List<? extends ReturnPoint<? super Concept>> getDependenciesReturnPoints() {
      if (this.dependencies == null)
        return Collections.emptyList();
      return new InfiniteSingletonList<ReturnPoint<Concept>>(new ReturnPointAdapter<Concept>() {
        public void manage(TaskId taskId, Exception exception) {
          IncorporateConcept.this.returnPoint.manage(IncorporateConcept.this.taskId, exception);          
        }
      }, this.dependencies.size());
    }

    @Override
    public List<? extends Task<Void>> getConsequences() {
      return this.consequences;
    }

    @Override
    public List<? extends ReturnPoint<? super Void>> getConsequencesReturnPoints() {
      if (this.consequences == null)
        return Collections.emptyList();
      return new InfiniteSingletonList<ReturnPoint<Void>>(new ReturnPointAdapter<Void>() {
        public void manage(TaskId taskId, Exception exception) {
          IncorporateConcept.this.returnPoint.manage(IncorporateConcept.this.taskId, exception);          
        }
      }, this.consequences.size());
    }
  }

  protected static class CreateTransientLink extends CreateLink implements InternalTask<Link> {
    private static final long serialVersionUID = -3092085910852398111L;

    public CreateTransientLink(TaskId taskId, Flow flow, Id containerId, Id target, IdFragment sourceStartPoint, Id[] sourceNextPoints) {
      super(flow, containerId, target, sourceStartPoint, sourceNextPoints);
      this.taskId = taskId;
    }

    @Override
    public void process(Container container) throws DuplicateElementException, TransactionException, MissingElementException {
      try {
        Context.transientTransaction.set(true);
        super.process(container);
      }
      finally {
        Context.transientTransaction.remove();
      }
    }
  }

  protected static class ChainCreateTransientLink extends ChainCreateLink implements InternalTask<Link> {
    private static final long serialVersionUID = -7554310252648353086L;

    public ChainCreateTransientLink(TaskId taskId, Flow flow, Id target) {
      super(flow, target);
      this.taskId = taskId;
    }

    @Override
    public void process(Container container) throws DuplicateElementException, TransactionException, WrongStateException {
      try {
        Context.transientTransaction.set(true);
        super.process(container);
      }
      finally {
        Context.transientTransaction.remove();
      }
    }
  }

  protected static class CreateTransientConcept extends CreateConcept implements InternalTask<Concept> {
    private static final long serialVersionUID = -7836241267796710829L;

    public CreateTransientConcept(TaskId taskId, Id targetContainerId, IdFragment conceptId) {
      super(targetContainerId, conceptId);
      this.taskId = taskId;
    }

    @Override
    public void process(Container container) throws DuplicateElementException {
      try {
        Context.transientTransaction.set(true);
        super.process(container);
      }
      finally {
        Context.transientTransaction.remove();
      }
    }
  }

  /**
   * Creates a new {@link Primitive} inside the {@link Context} that will be not pushed to associated Clouds.
   * If the same {@link Primitive} already exists, then returns it through the {@link ReturnPoint} as if it
   * was created and doesn't throws a {@link DuplicateElementException}.
   */
  protected static class CreateTransientPrimitive extends CreatePrimitive implements InternalTask<Primitive> {
    private static final long serialVersionUID = -1666611933708212364L;

    public CreateTransientPrimitive(TaskId taskId, Id targetContainerId, IdFragment conceptId, Object value) {
      super(targetContainerId, conceptId, value);
      this.taskId = taskId;
    }

    @Override
    public void process(Container modifiable) throws DuplicateElementException {
      // Primitive is already there?
      Concept aux = modifiable.getConcept(this.conceptId);
      if ((aux != null) && (aux instanceof Primitive) && (this.value.equals(((Primitive)aux).getValue())))
        this.returnPoint.manage(this.taskId, (Primitive)aux);
      else {
        try {
          Context.transientTransaction.set(true);
          super.process(modifiable);
        }
        finally {
          Context.transientTransaction.remove();
        }
      }
    }
  }

  /** Deletes a {@link Connectable} inside the {@link Context}. This erase will be not pushed to associated Clouds. */
  protected static class TransientDeleteConnectable extends DeleteConnectable implements InternalTask<Connectable> {
    private static final long serialVersionUID = -7873541254145006222L;

    public TransientDeleteConnectable(TaskId taskId, Id targetContainerId, IdFragment startPoint, Id ... nextPoints) {
      super(targetContainerId, startPoint, nextPoints);
      this.taskId = taskId;
    }

    @Override
    public void process(Container modifiable) throws NonDeletableElementException, TransactionException, MissingElementException {
      try {
        Context.transientTransaction.set(true);
        super.process(modifiable);
      }
      finally {
        Context.transientTransaction.remove();
      }
    }
  }

  protected class ContextContainerImpl extends ContainerImpl {
    private static final long serialVersionUID = -7039313798990487921L;

    public ContextContainerImpl(long creationTime, TransactionId transactionId, IdFragment idFragment) {
      super(creationTime, transactionId, idFragment);
    }

    /** Returns true if the concept was incorporated, and false if not (bacause already exists).*/
    protected boolean incorporate(Concept concept) {
      try {
        IdFragment cidf = concept.getLocalIdFragment();
        Concept prev = this.getConcept(cidf);
        if ((prev != null) && !(prev instanceof Dummy))
          return false; // Already added.
        if (prev instanceof Dummy)
          this.identifiables.remove(cidf);
        this.insertConceptImpl(null, cidf, (ConceptImpl)concept);
      }
      catch (Exception ex) {
        logger.error(ex.getMessage(), ex); // Should never happen.
      }
      return true;
    }

    @Override
    protected <R> void routeToNext(Task<R> task, int commonAncestors, ReturnPoint<? super R> rp) throws ScireException {
      try {
        IdFragment containerId = task.getTargetContainerId().getIdFragment(commonAncestors);
        ContextContainerImpl container;
        Identifiable identifiable = this.identifiables.get(containerId);
        if ((identifiable == null) || (identifiable instanceof Dummy)) {
          container = new ContextContainerImpl(System.currentTimeMillis(), null, containerId);
          List<Link> links = Context.this.connection.submit(new GetContainerLinks(new Id(this.getId(), containerId))).get();
          if (links != null)
            for (Link link: links)
              container.incorporateLink(link);
          this.process(new IncorporateConcept(Context.this.internalTransaction.nextTaskId(), this.getId(), container), logErrorReturnPoint);
        }
        else if (identifiable instanceof ContextContainerImpl)
          container = (ContextContainerImpl)identifiable;
        else
          throw new WrongStateException("Routing asks for a Container, but we have here other thing: " + task.getTargetContainerId());
        container.route(task, commonAncestors+1, rp);
      }
      catch (Exception ex) {
        rp.manage(task.getTaskId(), ex);
      }
    }

    @Override
    protected <R> void routeToPrev(Task<R> task, int commonAncestors, ReturnPoint<? super R> rp) throws ScireException {
      if (commonAncestors == 0) {
        IdFragment rootId = task.getTargetContainerId().getIdFragment(0);
        ContextContainerImpl container = Context.this.roots.get(rootId);
        if (container == null) {
          container = new ContextContainerImpl(System.currentTimeMillis(), null, rootId);
          ContextContainerImpl prev = Context.this.roots.putIfAbsent(rootId, container);
          if (prev != null)
            container = prev;
        }
        container.route(task, 1, rp);
      }
      else
        super.routeToPrev(task, commonAncestors, rp);
    }

    @Override
    protected <T extends ConceptImpl> T insertConceptImpl(TransactionId transactionId, IdFragment idf, T concept) throws DuplicateElementException {
      T result = super.insertConceptImpl(transactionId, idf, concept);
      Context.this.fireAddedEvent(result);
      return result;
    };

    @Override
    protected Link createLinkImpl(long creationTime, TransactionId transactionId, Flow flow, ConnectableImpl source, Id target) throws TransactionException, DuplicateElementException {
      Link result = super.createLinkImpl(creationTime, transactionId, flow, source, target);
      // Context.this.fireUpdatedEvent(source);
      Context.this.fireAddedEvent(result);
      return result;
    }

    @Override
    public Primitive setPrimitiveValue(long modificationTime, TransactionId transactionId, IdFragment idf, Object value) throws MissingElementException, WrongStateException, TransactionException {
      Primitive result = super.setPrimitiveValue(modificationTime, transactionId, idf, value);
      Context.this.fireUpdatedEvent(result);
      return result;
    }
    
    @Override
    protected void markAsDeleted(TransactionId transactionId, ConnectableImpl connectable) throws TransactionException {
      super.markAsDeleted(transactionId, connectable);
      Context.this.fireDeletedEvent(connectable);
    }

    @Override
    protected void addReverseLinkReference(LinkImpl link, Id target) {
      super.addReverseLinkReference(link, target);
    }

    @Override
    public void trackExternalLinks(IdFragment targetConcept, Id externalContainer, int number) throws MissingElementException {
      if (!this.contains(targetConcept)) {
        try {
          Context.this.pull(new GetConcepts(this.getId(), targetConcept));
        }
        catch (Exception ex) {
          logger.error(ex.getMessage(), ex);
        }
      }
      super.trackExternalLinks(targetConcept, externalContainer, number);
    }

    public void clear() {
      Iterator<Map.Entry<IdFragment, Identifiable>> entries = this.identifiables.entrySet().iterator();
      while (entries.hasNext()) {
        Map.Entry<IdFragment, Identifiable> entry = entries.next();
        Identifiable element = entry.getValue();
        entries.remove();
        if (element instanceof ContextContainerImpl)
          ((ContextContainerImpl)element).clear();
        else
          Context.this.fireDeletedEvent((Connectable)element);
      }
      this.identifiables.clear();
      this.reverseLinkReferences.clear();
      this.externalReferences.clear();
      this.transactionPendingConnectables.clear();
    }

    public void getTransactionPendingConnectables(TransactionId transaction, List<Connectable> result) {
      Set<ConnectableImpl> pending = this.transactionPendingConnectables.get(transaction);
      if (pending != null)
          result.addAll(pending);
      for (Identifiable i: this.identifiables.values())
        if (i instanceof ContextContainerImpl)
          ((ContextContainerImpl)i).getTransactionPendingConnectables(transaction, result);
    }

    public void commitPushTransaction() {
      this.process(new CommitTask(pushTransaction.nextTaskId(), this.getId()), logErrorReturnPoint);
      for (Identifiable i: this.identifiables.values())
        if (i instanceof ContextContainerImpl)
          ((ContextContainerImpl)i).commitPushTransaction();
    }

    protected void incorporateLink(Link link) throws DuplicateElementException {
      this.connect(this, (LinkImpl)link);
    }
  }

  protected static class Dummy extends ConceptImpl {
    private static final long serialVersionUID = 2067932752061304406L;

    protected Dummy(Id id) {
      super(System.currentTimeMillis(), null, id.getLastIdFragment());
      this.id = id;
    }
  }

  protected class GetContainerLinks extends Query<Link> {
    private static final long serialVersionUID = -423582669472666504L;

    public GetContainerLinks(Id targetContainerId) {
      super(targetContainerId);
    }

    @Override
    public void process(Container targetContainer) {
      this.returnPoint.manage(this.taskId, targetContainer.getLinks().values());
    }
  }

  private ConcurrentMap<IdFragment, ContextContainerImpl> roots;
  private Connection connection;
  private List<ContextListener> listeners;
  private Transaction internalTransaction;
  private Transaction pushTransaction;
  private ContextChangesListener changesListener;

  public Context(Connection connection) {
    this.roots      = new ConcurrentSkipListMap<IdFragment, ContextContainerImpl>();
    this.connection = connection;
    this.connection.setAutocommit(false);
    this.listeners  = new CopyOnWriteArrayList<ContextListener>();
    this.internalTransaction = new Transaction(true);
    this.pushTransaction     = new Transaction(false);
    this.changesListener     = new ContextChangesListener();
    this.addContextListener(this.changesListener);
  }

  public void addContextListener(ContextListener listener) {
    this.listeners.add(listener);
  }

  public void removeContextListener(ContextListener listener) {
    this.listeners.remove(listener);
  }

  protected void fireAddedEvent(Connectable element) {
    if (!((element instanceof Dummy) || (Context.transientTransaction.get() == Boolean.TRUE)))
      for (ContextListener listener: this.listeners)
        listener.added(element);
  }

  protected void fireUpdatedEvent(Connectable element) {
    if (!((element instanceof Dummy) || (Context.transientTransaction.get() == Boolean.TRUE)))
      for (ContextListener listener: this.listeners)
        listener.updated(element);
  }  

  protected void fireDeletedEvent(Connectable element) {
    if (!((element instanceof Dummy) || (Context.transientTransaction.get() == Boolean.TRUE)))
      for (ContextListener listener: this.listeners)
        listener.deleted(element);
  }

  public <T extends Connectable> List<T> pull(Query<T> query) throws InterruptedException, ExecutionException {
    List<T> result = (List<T>)connection.submit(query).get();
    if (result != null)
      for (T element: result)
        this.add(element);
    return result;
  }

  private void add(Connectable connectable) throws InterruptedException, ExecutionException {
    while (connectable instanceof Link)
      connectable = ((Link)connectable).getSource();
    Concept concept = (Concept)connectable;
    process(new IncorporateConcept(internalTransaction.nextTaskId(), concept)).get();
  }

  /**
   * Creates a new {@link CreateLink} to be processed only through {@link Context}'s
   * {@link Context#process(Task)} method and that will create a transient {@link Link} not to be pushed
   * to associated Clouds.
   * 
   * @param flow Link.Flow that will have the {@link Link}.
   * @param containerId {@link Id} of the {@link Container} that holds the {@link Link}'s source.
   * @param target {@link Id} of the {@link Link} target.
   *        It must be from a {@link Identifiable} and {@link Connectable}.
   * @param sourceStartPoint {@link IdFragment} of the {@link Identifiable} origin of the source of the {@link Link}.
   *        It must belong to <code>this</code> and be a  {@link Connectable}.
   * @param sourceNextPoints if the source is another {@link Link}, this {@link Id} array will contain the
   *        target identifiers to which connect the 'parent' {@link Link}s of such source.
   */
  public CreateLink newCreateTransientLink(Flow flow, Id containerId, Id target, IdFragment sourceStartPoint, Id ... sourceNextPoints) {
    return new CreateTransientLink(internalTransaction.nextTaskId(), flow, containerId, target, sourceStartPoint, sourceNextPoints);
  }

  /**
   * Creates a new {@link ChainCreateLink} to be processed only through {@link Context}'s
   * {@link Context#process(Task)} method and that will create a transient {@link Link} not to be pushed
   * to associated Clouds.
   * 
   * @param flow Link.Flow that will have the {@link Link}.
   * @param target {@link Id} of the {@link Link} target.
   *        It must be from a {@link Identifiable} and {@link Connectable}.
   */
  public ChainCreateLink newCreateTransientLink(Flow flow, Id target) {
    return new ChainCreateTransientLink(internalTransaction.nextTaskId(), flow, target);
  }

  /**
   * Creates a new {@link CreateConcept} to be processed only through {@link Context}'s
   * {@link Context#process(Task)} method and that will create a transient {@link Concept} not to be pushed
   * to associated Clouds.
   * 
   * @param id Id of the new {@link Concept}.
   */
  public CreateConcept newCreateTransientConcept(Id id) {
    return new CreateTransientConcept(internalTransaction.nextTaskId(), id.getParentId(), id.getLastIdFragment());
  }

  /**
   * Creates a new {@link CreatePrimitive} to be processed only through {@link Context}'s
   * {@link Context#process(Task)} method and that will create a transient {@link Primitive} not to be pushed
   * to associated Clouds.
   * If the same {@link Primitive} already exists, then the given {@link CreatePrimitive} returns it through the
   * {@link ReturnPoint} as if it was created and doesn't throws a {@link DuplicateElementException}.
   * 
   * @param id Id of the new {@link Primitive}.
   * @param value Object that will hold the new {@link Primitive}. 
   */
  public CreatePrimitive newCreateTransientPrimitive(Id id, Object value) {
    return new CreateTransientPrimitive(internalTransaction.nextTaskId(), id.getParentId(), id.getLastIdFragment(), value);
  }

  /**
   * Create a {@link DeleteConnectable} to be processed only through {@link Context}'s
   * {@link Context#process(Task)} method and does a transient delete of a {@link Connectable} inside the
   * {@link Context} not pushed to associated Clouds.
   * 
   * @param startPointId {@link Id} identifying the {@link Connectable} first point or source. If an {@link Identifiable}, this will be its {@link Id}.
   * @param nextPoints {@link Id} array identifying {@link Link}s targets that leads to the one to be deleted.
   */
  public DeleteConnectable newDeleteConnectable(Id startPointId, Id ... nextPoints) {
    return new TransientDeleteConnectable(internalTransaction.nextTaskId(), startPointId.getParentId(), startPointId.getLastIdFragment(), nextPoints);
  }

  public <T> Future<List<T>> process(Task<T> task) {
    FutureReturnPoint<T> future = new FutureReturnPoint<T>();
    task.setTaskId(pushTransaction.nextTaskId());
    this.route(task, future);
    return future;
  }

  protected <T> void route(Task<T> task, ReturnPoint<? super T> rp) {
    try {
      IdFragment rootId = task.getTargetContainerId().getIdFragment(0);
      ContextContainerImpl container = this.roots.get(rootId);
      if (container == null) {
        container = new ContextContainerImpl(System.currentTimeMillis(), null, rootId);
        ContextContainerImpl prev = this.roots.putIfAbsent(rootId, container);
        if (prev != null)
          container = prev;
//        else
//          getAndIncorporateLinks(container); // Root containers can't have links.
      }
      container.route(task, rp);
    }
    catch (Exception ex) {
      rp.manage(task.getTaskId(), ex);
    }
  }

  // FIXME: Make this thread safe ...
  public void reset() {
    for (ContextContainerImpl root: this.roots.values())
      root.clear();
    this.roots.clear();
    this.pushTransaction = new Transaction(false);
    this.changesListener.clear();
  }

  // FIXME: Make this thread safe ...
  public void push() {
    try {
      List<Modification<? extends Connectable>> changes = this.changesListener.extractOrderedChanges();
      for (Modification<? extends Connectable> task: changes)
        this.connection.submit(task).get();
      this.connection.commit();
      for (ContextContainerImpl root: this.roots.values())
        root.commitPushTransaction();
      this.pushTransaction = new Transaction(false);
    }
    catch (Exception ex) {
      logger.error(ex.getMessage(), ex);
      try {
        this.connection.rollback();
      }
      catch (Exception ex2) {
        logger.error(ex2.getMessage(), ex2);
      }
    }
  }
}